home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
PC Media 20
/
PC MEDIA CD20.iso
/
share
/
prog
/
cursoasm
/
cap6.msg
< prev
next >
Wrap
Text File
|
1993-07-05
|
18KB
|
293 lines
INTRODUCCION AL ASM: LAS INTERRUPCIONES
=======================================
Como comentábamos al final del capítulo dedicado a la pila, en este capítulo
vamos a abordar el tema de las interrupciones. Comencemos pues.
Al principio del curso, vimos el funcionamiento del uP, que contínuamente
recoge instrucciones de la memoria (de la dirección apuntada por CS:IP) y las
ejecuta. Aunque éste es el funcionamiento habitual, hay momentos en los que
esta actividad se tiene que interrumpir. Para ver la razón de la existencia de
las interrupciones, veamos un ejemplo.
Supongamos que el funcionamiento del teclado fuese el siguiente: el programa
enviaría una señal al teclado, a la que el teclado respondería con un código
indicativo de la tecla. Existiría también un código para indicar 'ninguna
tecla pulsada'. Dentro de este esquema de funcionamiento, imaginemos que un
programa está en un momento dado llevando a cabo una actividad de cálculo, sin
comprobar para nada lo que ocurre con el teclado. Es perfectamente posible que
en ese momento el usuario del ordenador pulse una tecla y la suelte antes de
que el programa compruebe el teclado; si el funcionamiento del teclado fuese el
que hemos visto, esa pulsación de tecla se perdería. Existen varias posibles
soluciones a este problema: una sería que el programa comprobase constantemente
el teclado, con la consiguiente molestia al desarrollar el programa y la pérdi-
da de velocidad. Otra sería añadir hardware al teclado para llevar un buffer
con las teclas pulsadas. Esto ocurre así en los PCs, pero este buffer sólo
tiene capacidad para unas cuatro pulsaciones, con lo que el problema sigue pre-
sentándose. La solución más adecuada, ya que se puede extender a otros proble-
mas parecidos, son las interrupciones hardware, que además es la solución
implementada en los PCs.
Cuando algún periférico necesita que el uP se encargue de algo con urgencia
(por ejemplo, leer el valor de una tecla) pone a nivel activo uno de los pines
del uP, denominado INTR. En este momento, el uP para lo que está haciendo (si
está en medio de la ejecución de una instrucción, se completa ésta primero) y
lee el valor de 8 bits (0 a 255d) de las 8 líneas bajas del bus de datos. El
dispositivo que provoca la interrupción se debe haber encargado de poner en
éstas líneas un código que identifica el número de interrupción. Este número es
un código que identifica el dispositivo que ha provocado la interrupción; por
ejemplo, el teclado tiene el número 9 como identificativo y por tanto pone en
las 8 líneas bajas del bus de datos el valor 00001001b cuando se pulsa una te-
cla. Ahora, lo que ocurre es lo siguiente: el uP no está especialmente diseñado
para manejar algunos dispositivos en concreto, por lo que cuando recibe una in-
terrupción lo que hace es llamar a una rutina que el programador ha dejado re-
sidente en la memoria, que es la que lee la tecla pulsada y la almacena en un
buffer en memoria en el caso de la interrupción 9 (para otros números de inte-
rrupción, se lleva a cabo otra tarea). Esta rutina se suele denominar 'rutina
de servicio a la interrupción', por razones bastante claras (eso espero). La
última instrucción de la rutina de servicio a la interrupción es la instrucción
IRET, que hace que el uP vuelva a la dirección donde estaba antes de la inte-
rrupción. Aún nos queda por ver cómo el uP, a partir del número recibido por el
bus de datos con el número de interrupción, identifica a qué rutina ha de
saltar.
Los diseñadores del 8086 reservaron los primeros 4*256d bytes de la memoria
(1 kilobyte desde la dirección absoluta 00000h hasta la 003FFh, ambas incluí-
das) para una tabla denominada 'tabla de vectores de interrupción'. Esta tabla
contiene cuatro bytes para cada número de interrupción (por eso son 4*256d
bytes, 4 bytes/nº de interrupcion * 256d números de interrupción posibles), que
especifican la dirección de memoria donde comienza la rutina de servicio a cada
interrupción. Al recibir una demanda de interrupción, el micro toma el valor
del bus de datos (el nº de interrupción) y lo multiplica por cuatro. Usa el va-
lor obtenido como dirección de la que leer 4 bytes con la dirección a la que
saltar. Estos cuatro bytes son el offset y el segmento que forman la dirección
absoluta. El ordenamiento Intel especifica que se almacenan primero los bytes
de menor peso, y en el caso del 8086 se almacena primero el offset que el valor
de segmento. Por ejemplo, si instalásemos una rutina de servicio a la interrup-
ción CCh en la dirección de memoria 1122h:3344h, tendríamos que introducir los
siguientes valores en la tabla de vectores de interrupción:
DIRECCION ABSOLUTA VALOR
------------------ -----
4 * CCh = 816d 44h
4 * CCh + 1 = 817d 33h
4 * CCh + 2 = 818d 22h
4 * CCh + 3 = 819d 11h
Este es el modelo de funcionamiento, pero todavía no hemos vistos varios
detalles muy importantes. En realidad, la arquitectura del PC hace que los
periféricos no interrumpan directamente al uP (¿qué ocurriría si dos periféri-
cos deciden generar una señal de interrupción a la vez?), sino que éstas están
jerarquizadas mediante unos chips denominados 8259 o PIC ('Programmable
interrupt controller', Controlador de Interrupciones Programable), interactuan-
do de una manera bastante compleja. De momento, esto no nos interesa demasiado.
Cuando el micro recibe la señal de interrupción, el CS y el IP contienen la
dirección de la siguiente instrucción a ejecutar (ya que el IP se incrementa
nada más cargar la instrucción, antes incluso de ejecutarla). Estos se cargan
con los valores leídos de la tabla de vectores de interrupción, por lo que es
necesario preservar su valor actual para luego volver al punto donde se estaba
cuando se produjo la interrupción. Lo que se hace es empujar estos valores en
la pila antes de saltar a la rutina de servicio a la interrupción. Además,
antes de empujar el CS y el IP (en este orden), se empuja el contenido de un
registro que todavía no hemos visto (lo veremos dentro de poco), llamado
'registro de flags'. Los bits de este registro tienen cada uno un significado
especial, indicando en todo momento el estado actual del uP y, en el caso de
algunos bits, el resultado de la última operación ejecutada (por ejemplo, el
flag ZERO indica si la última operación dio un resultado de 0). Ya que algunos
flags se utilizan para indicar si se esta ejecutando una rutina de servicio a
la interrupción (y por tanto se modifican antes de saltar a la rutina), este
registro se salva a fin de que al volver al hilo normal de ejecución del pro-
grama se restaure completamente el estado anterior de micro.
Ya que una interrupción se puede producir en cualquier momento, ésta debe
ser transparente al programa en ejecución. Cuando se escribe un programa, no
se piensa en que entre una instrucción y la siguiente pueden ejecutarse 200
instrucciones que leen un valor de teclado y lo almacenan en un buffer en
memoria. Por tanto, al volver de una interrupción se tiene que restaurar por
completo el estado del uP, de forma que el programa interrumpido no se vea
afectado. Por ejemplo, supongamos un fragmento de código así:
MOV AX,[200h]
ADD AX,30h
Si en medio de las dos instrucciones se ejecuta una rutina de servicio a la
interrupción que modifica el contenido de AX, el resultado para el programa
puede ser catastrófico. El uP se encarga de que el registro de flags sea res-
taurado, ya que siempre es modificado. Pero es la propia rutina de servicio a
la interrupción la que debe preservar el contenido de los registros que ésta
modifique. Por tanto, las rutinas de servicio a la interrupción suelen tener el
siguiente aspecto:
PUSH AX
PUSH BX
PUSH CX
PUSH DX
PUSH DS
PUSH ES
PUSH BP
PUSH SI
PUSH DI
[...] ;código que modifica los anteriores 9 registros
POP DI
POP SI
POP BP
POP ES
POP DS
POP DX
POP CX
POP BX
POP AX
IRET ;retorna al programa interrumpido
Otro de los aspectos importantes de las interrupciones es el de la pila.
Hemos visto que el uP empuja el contenido de los flags, el de CS y el de IP en
la pila apuntada por SS:SP en el momento de la interrupción. Aunque la rutina
de servicio a la interrupción podría modificar SS y SP para usar su propia
pila, esto no es lo habitual; es decir, las rutinas de servicio a la interrup-
ción suelen utilizar la pila del programa interrumpido. Por tanto, aunque vimos
que un POP no borra el valor apuntado por SS:SP después de leerlo, no se puede
suponer que después de un POP los valores inmediatamente por debajo de SS:SP
continúen igual, ya que pueden haber sido pisados debido a una interrupción.
En realidad, cuando se pone a nivel activo la patilla INTR, el uP no siempre
interrumpe el programa en ejecución, sino que primero consulta uno de los bits
del registro de flags, llamado IF ('Interruption Flag', Flag de Interrupción)
y sólo si este está a 1 se atiende la interrupción. En caso de que IF está a 0,
simplemente se ignora la petición de interrupción. Es el 8259 (el PIC) el que
se encarga de volver a generar la interrupción repetidas veces hasta que el uP
está listo. Cuando se salta a una rutina de servicio a la interrupción, una de
las cosas que se hace es poner este bit a 1 de manera que no se pueda interrum-
pir la propia rutina de servicio a la interrupción. Pero esta característica la
puede aprovechar cualquier programa, ya que existen dos instrucciones que ponen
a 0 o a 1 el flag IF:
STI Pone a 1 el bit IF, para que se atienda a las
peticiones de interrupción.
CLI Pone a 0 el bit IF, para desactivar las interrupciones.
Debido a que se puede controlar el uP para que atienda o no a las peticiones
de servicio a la interrupción, este tipo de interrupciones se denominan
'Interrupciones enmascarables'; existe otra patilla denominada 'NMI' ('Non
Maskable Interrupt', Interrupción No Enmascarable) de función parecida a INTR,
pero a cuya petición el uP siempre responde. En realidad, no sé con certeza
cómo funcionan las NMI, ya que no se usan habitualmente para la programación.
En el Spectrum, se utilizaba esta interrupción para los 'transfer', aparatos
que permitian en cualquier momento generar una NMI, por medio de un pulsador, y
que se grabara a cinta o disco los registros del uP y los contenidos de toda la
memoria (con cualquier programa que ésta tuviera), para luego poder cargarlo y
continuar la ejecución en el mismo punto; estos eran copiadores infalibles.
Todo esto que hemos visto hasta ahora es interesante para nosotros, pero en
realidad es al diseñador de hardware y al programador de drivers de dispositi-
vos a quien más le concierne. Además, las interrupciones hardware son aún más
complicadas, ya que hay que controlar el PIC, etc... En realidad, sólo en algu-
nos pocos casos nos interesará todo esto (el caso más habitual es el de querer
instalar una rutina de servicio a la interrupción de teclado para poder detec-
tar la pulsación simultánea de varias teclas, como en los juegos). Estas inte-
rrupciones que hemos visto se denominan interrupciones hardware, pero lo que
principalmente nos interesa son las interrupciones software.
Existe una instrucción en ASM llamada INT, que fuerza al uP a realizar el
mismo proceso que lleva a cabo para atender una interrupción. El número de
interrupción no se lee del bus de datos, sino que viene dado por el operando de
la instrucción INT. Por ejemplo, puede usarse la instrucción 'INT 9' para lla-
mar a la rutina de servicio a la interrupción de teclado. En realidad, éste no
es el uso habitual, ya que, por ejemplo, la rutina de servicio a la interrup-
ción 9 espera que el teclado tenga una pulsación de tecla que enviar, por lo
que el funcionamiento de ésta sería anómalo al invocar la INT 9 sin ninguna te-
cla esperando. En realidad, la utilidad de las interrupciones software es otra.
Una parte muy importante de un sistema operativo (quizá la más importante)
es el 'kernel' o núcleo. Este kernel no es más que un conjunto de rutinas o
funciones que ejecutan diversas tareas, como por ejemplo escribir una cadena en
el monitor o escribir datos en un fichero. Estas funciones pueden estar en dis-
tintas direcciones de memoria en distintos ordenadores, por lo que es necesaria
una manera de encontrarlas. En el caso del MS-DOS, la mayoría de los servicios
del kernel están integrados en una función que lleva a cabo distintas acciones
según el contenido del registro AH a la entrada; a cada tarea distinta que se
puede ejecutar en función de AH se le denomina 'servicio'. En los demás regis-
tros se pasan parámetros, cuyo significado varía de un servicio a otro; en caso
de que sea necesario devolver algún valor, se devuelve en los registros. Por
ejemplo, el servicio 9 (AH = 9) imprime una cadena en la pantalla. La cadena se
recoge de la dirección dada por el par DS:DX, y el final de ésta se indica con
el carácter '$'. La función que ofrece todos estos servicios se instala en
memoria cuando se carga el sistema operativo de los ficheros IO.SYS y
MSDOS.SYS. Pero cada vez que se carga puede hacerse en una dirección distinta,
por lo que se almacena en el vector número 21h de la tabla de vectores de inte-
rrupción la dirección donde reside la rutina. Por lo tanto, cuando un programa
quiere invocar algún servicio lo que se hace es ejecutar una instrucción 'INT
21h'. A modo de ejemplo, veamos qué haríamos para imprimir una cadena almacena-
da en la dirección 1234h:ABCDh :
PUSH DS ; preserva el contenido de DS
MOV AX,1234h ; segmento de la dir. de la cadena
MOV DS,AX ; en DS
MOV DX,ABCDh ; offset de la dir. de la cadena
MOV AH,9 ; servicio 9: escribir cadena
INT 21h ; invoca la función del DOS
POP DS ; restaura DS
La interrupción 21h nunca se invoca a causa de una interrupción hardware,
por lo que no se ejecuta entre dos instrucciones cualquiera. Cuando ésta se
ejecuta, es debido a que el programa la ha invocado expresamente. Por tanto, no
es necesario preservar completamente el estado del uP, y de hecho no se hace.
Muchos de los servicios de la interrupción 21h devuelven valores en los regis-
tros, por lo que necesariamente son modificados; en los servicios de ficheros
un flag se utiliza para indicar si ha habido un error, y en caso de que lo haya
habido AX contiene un código de error. SS y SP siempre se conservan (por el
propio funcionamiento de las interrupciones, si la rutina modificase estos re-
gistros no se podría volver al programa), pero los demás registros no siempre
lo hacen. En los manuales de referencia de las interrupciones del DOS suele
venir especificado qué registros se conservan, pero lo habitual es empujar los
registros cuyo contenido interesa conservar en lugar de consultar la referen-
cia. A no ser que se use para devolver algún valor, se suele suponer que DS se
conserva. Si interesa conservar el contenido de algún otro registro, es mejor
empujarlo en la pila.
El BIOS también es, principalmente, un conjunto de funciones que el progra-
mador puede usar. Las funciones son de más bajo nivel que las del DOS, y de
hecho las funciones del DOS suelen valerse de llamadas a la BIOS para cumplir
su tarea. La BIOS puede variar de una placa a otra, pero siempre debe propor-
cionar algunas funciones que vienen definidas por el estándar PC, como por
ejemplo la función para escribir un carácter en la pantalla o la función para
poner un pixel a un color determinado, usada en modo gráfico. Las funciones
gráficas del BIOS están agrupadas en la INT 10h, siendo AH el que especifica el
servicio o función en concreto, y, en algunas funciones, AL la subfunción.
Con lo que ya sabemos, podemos utilizar las interrupciones del BIOS y del
DOS sin más problema. Al igual que para programar en un lenguaje como Pascal o
C es tan importante conocer la sintáxis como las funciones que nos da la
librería estándar de nuestro compilador, para programar en ASM también es nece-
sario conocer las interrupciones del BIOS y el DOS. Que yo sepa, nadie se
aprende los servicios de memoria, por lo que lo habitual es programar con un
manual de referencia de las interrupciones. Por poco dinero, encontraréis en
cualquier librería un libro con estos datos. Se hace casi imprescindible para
programar en ASM, por lo que os recomiendo que os hagáis con uno. Además, a
partir de ahora escribiremos programitas en ASM que habrá que ensamblar y
linkar, por lo que os hará falta el Macro Assembler de Microsoft (abreviadamen-
te MASM) o el Turbo Assembler de Borland (TASM). No os voy a recomendar ninguno
personalmente, ya que las características principales de ambos son iguales,
pero debería transmitiros la opinión generalizada del área de programación ASM
de FidoNET, que se decanta claramente por el TASM. De todas maneras, en el
curso no usaremos características particulares de ninguno de los dos, por lo
que podréis usar cualquiera de los dos para seguirlo.
En el capítulo que viene veremos el 'Hello, world' en ASM, con el objetivo
principal de ver el esqueleto de un listado ASM para el MASM o el TASM. Segui-
remos después con el juego de instrucciones, viendo los flags y los saltos
primero. Cuando conozcamos la mayoría de las instrucciones, comenzaremos a ver
las tareas principales de cualquier programa: interacción con el usuario,
manejo de ficheros, gestión de memoria, etc... Si alguien tiene interés en al-
gún tema en especial, no tiene más que comentármelo. Personalmente, me gustaría
ver algo de programación gráfica, que es mi hobby preferido, pero sólo si hay
gente interesada. Dentro de unos pocos capítulos, iremos viendo a la vez
nuevas instrucciones y servicios que proporcionan el DOS y el BIOS, ya que esto
es un curso y no una referencia de ASM. Al igual que hasta ahora, no busco dar
una definición teórica del ASM (esto no es la Universidad), sino que quiero que
quien siga el curso pueda hacer en ASM todo lo que hace en otros lenguajes.
Salut :-)
Jon